package com.wzw.expression.record.proxy;

import com.wzw.expression.expression.invocation.OperationInvocationCache;
import com.wzw.expression.expression.operation.Operation;
import com.wzw.expression.expression.source.OperationSource;
import com.wzw.expression.record.expression.*;
import com.wzw.expression.record.model.IRecordContext;
import com.wzw.expression.record.support.IRecordTraces;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.expression.ParserContext;
import org.springframework.lang.NonNull;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Advice
 * IRecord 增强
 *
 * @author Wangzhiwen
 */
@Slf4j
public class IRecordMethodInterceptor implements MethodInterceptor, BeanFactoryAware, DisposableBean, Serializable {

    private final OperationSource recordOperationSource;

    private final IRecordExpressionEvaluator evaluator = new IRecordExpressionEvaluator();

    private final OperationInvocationCache recordOperationInvocationCache;

    public IRecordMethodInterceptor(OperationSource recordOperationSource, OperationInvocationCache recordOperationInvocationCache) {
        this.recordOperationSource = recordOperationSource;
        this.recordOperationInvocationCache = recordOperationInvocationCache;
        evaluator.setParserContext(ParserContext.TEMPLATE_EXPRESSION);
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        Method method = invocation.getMethod();
        Object[] arguments = invocation.getArguments();
        Object invocationThis = invocation.getThis();
        assert invocationThis != null;
        Class<?> targetClass = AopProxyUtils.ultimateTargetClass(invocationThis);
        Method targetMethod = (!Proxy.isProxyClass(targetClass) ? AopUtils.getMostSpecificMethod(method, targetClass) : method);
        // 解析注解获取参数
        Collection<Operation> operations = recordOperationSource.getOperations(method, targetClass);
        List<IRecordContext> contexts = recordOperationInvocationCache
                .getOperationContexts(operations, method, targetClass, arguments, invocationThis)
                .get(IRecordOperation.class)
                .stream().map(v -> (IRecordContext) v).collect(Collectors.toList());
        if (contexts.isEmpty()) {
            return invocation.proceed();
        }
        IRecordEvaluationContext evaluationContext = evaluator.createEvaluationContext(method, arguments, invocationThis, targetClass, targetMethod, IRecordEvaluationContext.class);
        IRecordTraces.increment();
        before(evaluationContext, contexts);
        try {
            Object result = invocation.proceed();
            after(evaluationContext, contexts, result);
            return result;
        } catch (Throwable throwable) {
            exception(evaluationContext, contexts, throwable);
            throw throwable;
        } finally {
            IRecordTraces.decrement();
            eventual(evaluationContext, contexts);
            if (IRecordTraces.done()) {
                IRecordTraces.remove();
            }
        }
    }

    private void before(IRecordEvaluationContext evaluationContext, List<IRecordContext> contexts) {
        contexts.forEach(context -> {
            try {
                context.innerRecord();
                IRecordInvocationMetadata metadata = (IRecordInvocationMetadata) context.getMetadata();
                metadata.getInterceptor().before(context);
            } catch (Exception e) {
                log.error("IRecordInterceptor execute before error", e);
            }
        });
    }

    private void after(IRecordEvaluationContext evaluationContext, List<IRecordContext> contexts, Object result) {
        LocalDateTime now = LocalDateTime.now();
        contexts.forEach(context -> {
            try {
                IRecordInvocationMetadata metadata = (IRecordInvocationMetadata) context.getMetadata();
                IRecordOperation recordOperation = (IRecordOperation) metadata.getOperation();
                context.setSerial(evaluator.serial(recordOperation.getSerial(), metadata.getMethodKey(), evaluationContext));
                context.setContent(evaluator.content(recordOperation.getContent(), metadata.getMethodKey(), evaluationContext));
                context.setExtraRecord(evaluator.extra(recordOperation.getExtra(), metadata.getMethodKey(), evaluationContext));
                context.setResult(result);
                context.getInnerRecord().setEndTime(now);
                metadata.getInterceptor().after(context);
            } catch (Exception e) {
                log.error("IRecordInterceptor execute after error", e);
            }
        });
    }

    private void exception(IRecordEvaluationContext evaluationContext, List<IRecordContext> contexts, Throwable throwable) {
        LocalDateTime now = LocalDateTime.now();
        contexts.forEach(context -> {
            try {
                context.setException(throwable);
                context.getInnerRecord().setEndTime(now);
                ((IRecordInvocationMetadata) context.getMetadata()).getInterceptor().exception(context);
            } catch (Exception e) {
                log.error("IRecordInterceptor execute exception error", e);
            }
        });
    }

    private void eventual(IRecordEvaluationContext evaluationContext, List<IRecordContext> contexts) {
        contexts.forEach(context -> {
            try {
                ((IRecordInvocationMetadata) context.getMetadata()).getInterceptor().eventual(context);
            } catch (Exception e) {
                log.error("IRecordInterceptor execute eventual error", e);
            }
        });
    }

    @Override
    public void destroy() throws Exception {
        evaluator.clear();
    }

    @Override
    public void setBeanFactory(@NonNull BeanFactory beanFactory) throws BeansException {
        evaluator.setBeanFactoryResolver(new BeanFactoryResolver(beanFactory));
    }
}
